/*
 * Copyright (C) 2010 TopCoder Inc., All Rights Reserved.
 */
package com.hercules.xre.protocol.testing.handlers.imagecomparer;

import com.hercules.xre.protocol.testing.handlers.Helper;
import com.hercules.xre.protocol.testing.handlers.ImageComparer;
import com.hercules.xre.protocol.testing.handlers.ImageComparerException;

import java.awt.image.BufferedImage;
import java.awt.image.Raster;

/**
 * This class is default implementation of {@link ImageComparer} interface. This class compares pixel-by-pixel value of
 * the two images. E.g.: pixel in (x,y) in the first image will be compared with pixel in (x,y) in the second image.
 * This class supports configurable amount of tolerance of the difference in each pixel. Finally, the class also
 * supports the minimum threshold for the images to be differ.
 * <p>
 * <b>Thread safety:</b>This class is effectively thread safe (if the setters are called once at beginning).
 *
 * @author bramandia, 7realm
 * @version 1.0
 */
public class PixelBasedImageComparer implements ImageComparer {
    /**
     * Represents the threshold value in which the two images are considered different. Valid value: non-negative.
     * Initialized to 0. It has getter/setter. It is used in compare method.
     */
    private double threshold;
    /**
     * Represents the tolerance in which two pixel values considered different. Valid value: non-negative. Initialized
     * to 0. It has getter/setter. It is used in compare method.
     */
    private double tolerance;

    /**
     * Empty default constructor.
     */
    public PixelBasedImageComparer() {
        // empty
    }

    /**
     * Gets threshold value in which the two images are considered different.
     *
     * @return the current value of the namesake field
     */
    public double getThreshold() {
        return threshold;
    }

    /**
     * Sets threshold value in which the two images are considered different.
     *
     * @param threshold the new value of threshold value in which the two images are considered different
     * @throws IllegalArgumentException if argument is negative
     */
    public void setThreshold(double threshold) {
        // check argument
        checkNegative(threshold, "threshold");
        this.threshold = threshold;
    }

    /**
     * Gets the tolerance in which two pixel values considered different.
     *
     * @return the current value of the tolerance in which two pixel values considered different
     */
    public double getTolerance() {
        return tolerance;
    }

    /**
     * Sets the tolerance in which two pixel values considered different.
     *
     * @param tolerance the new value of the tolerance in which two pixel values considered different
     * @throws IllegalArgumentException if argument is negative
     */
    public void setTolerance(double tolerance) {
        // check argument
        checkNegative(tolerance, "tolerance");
        this.tolerance = tolerance;
    }

    /**
     * Compare the given images.
     *
     * @param image1 the first image, must not be null
     * @param image2 the second image, must not be null
     * @return true if they are equal, false otherwise
     * @throws IllegalArgumentException if any argument is invalid
     * @throws ImageComparerException if bounds or number of samples per pixel is different for two images
     */
    public boolean compare(BufferedImage image1, BufferedImage image2) throws ImageComparerException {
        // check argument
        Helper.checkNull(image1, "image1");
        Helper.checkNull(image2, "image2");

        // get rasters and check bounds
        Raster r1 = image1.getData();
        Raster r2 = image2.getData();
        if (!r1.getBounds().equals(r2.getBounds())) {
            throw new ImageComparerException("Bounds of two images are not equal. First image: " + r1.getBounds()
                + ", second image: " + r2.getBounds() + ".");
        }
        if (r1.getNumBands() != r2.getNumBands()) {
            throw new ImageComparerException("Number of samples per pixel differs for two images. First image: "
                + r1.getNumBands() + ", second image: " + r2.getNumBands() + ".");
        }

        // prepare arrays in order not to recreate them every time
        double totaldifference = 0;
        int numBands = r1.getNumBands();
        double[] pixel1 = new double[numBands];
        double[] pixel2 = new double[numBands];

        // compare images pixel by pixel
        int minX = r1.getMinX();
        int minY = r1.getMinY();
        int maxX = minX + r1.getWidth();
        int maxY = minY + r1.getHeight();
        for (int y = minY; y < maxY; y++) {
            for (int x = minX; x < maxX; x++) {
                // get pixel data for current point
                r1.getPixel(x, y, pixel1);
                r2.getPixel(x, y, pixel2);

                // calculate difference for current point
                double pointDifference = 0;
                for (int i = 0; i < numBands; i++) {
                    double pixelDifference = Math.abs(pixel1[i] - pixel2[i]);
                    if (pixelDifference > tolerance * pixel1[i]) {
                        pointDifference += pixelDifference;
                    }
                }
                pointDifference /= numBands;
                totaldifference += pointDifference;
            }
            // exit earlier if difference is too big
            if (totaldifference >= threshold) {
                return false;
            }
        }
        // if we do not exited earlier, then here we will have totaldifference < threshold
        return true;
    }

    /**
     * Check if given value is negative.
     *
     * @param value the value to check
     * @param name the name of value argument
     * @throws IllegalArgumentException in value is negative
     */
    private static void checkNegative(double value, String name) {
        if (value < 0) {
            throw new IllegalArgumentException("The value for '" + name + "' should be not negative.");
        }
    }
}
